'''
* Author:       Gladyshev Dmitriy (2021)
*
* Design Name:  ColormusicCC
* Description:  Программа для управления цветомузыкой
'''
"""
TODO
Стробоскоп
Вынести отправку данных в отдельный поток
Переделать на multiprocessing
Разбить проект на модули
Отключение ламп при выходе из программы
Режим освещения

"""

__version__ = "0.0.1a"

import os
import sys
import time
import json
import math

import numpy as np
import sounddevice as sounddev

import serial  # pyserial

from threading import Thread, Lock

# Qt
from PyQt5 import QtWidgets, QtCore, QtGui
from PyQt5.QtWidgets import QTableWidgetItem, QLabel, QInputDialog, QComboBox, QSystemTrayIcon
from PyQt5.QtWidgets import QMessageBox, QWidget, QMenu
from PyQt5.QtGui import QPixmap, QPainter, QColor, QBrush, QFont
from PyQt5.QtCore import Qt, QRect
# design
import mainform

settings = {
    "Serial": {
        "Port": "COM10",
        "Speed": 115200
    },
    "mode": 1,                          # Активный режим работы
    "sensitivityRYG": [100, 100, 100],  # чувствительность по каналам
}

# Индексы элементов списка leds. Соответственно цветам светодиодов.
RED = 0
GREEN = 1
BLUE = 2



soundDevice = None
block_duration = 20
low = 40
high = 2000
gain = 7

stop_thread = False
lock_stop_thread = Lock()

spectrum = []
lock_spectrum = Lock()
leftLevel = 0
rightLevel = 0

ser = None

# Флаг блокировки многократного логирования ошибки порта
lastErrorPort = False

# Путь к папке с настройками
datapath = ""

def messageBox(title, s):
    """ Отображение диалогового окна с сообщением

    title -- заголовок окна
    s -- сообщение
    """
    msg = QMessageBox()
    msg.setIcon(QMessageBox.Information)
    msg.setText(s)
    msg.setWindowTitle(title)
    msg.exec_()


def saveSettings():
    """ Сохранение настроек в файл """
    try:
        with open(datapath + 'settings.json', 'w') as f:
            json.dump(settings, f)
    except:
        messageBox("Критическая ошибка", "Ошибка сохранения файла настроек. Возможно нет прав доступа на запись.")


def loadSettings():
    """ Загрузка настроек из файла """
    global settings
    try:
        with open(datapath + 'settings.json') as f:
            settings = json.load(f)
    except FileNotFoundError:
        pass
    except:
        messageBox("Критическая ошибка", "Ошибка чтения файла настроек. Возможно нет прав доступа на чтение.")


def isWindows():
    """ Проверяет, под какой ОС запущено приложение. 

    Возвращает True, если Windows.
    """
    if os.name == "nt":
        return True
    else:
        return False


def openPort():
    """Открытие COM-порта"""
    global ser
    global lastErrorPort
    speed = settings["Serial"]["Speed"]
    ok = True
    port = settings["Serial"]["Port"]

    try:
        #ser = serial.Serial(port, speed)
        ser = serial.Serial("COM4", 115200)
    except:
        ok = False
    if ok:
        print("Открытие COM-порта: " + ser.name)
        lastErrorPort = False
        time.sleep(2)
    else:
        if not lastErrorPort:
            print("ОШИБКА: Не удалось открыть COM-порт.")
            lastErrorPort = True
    return ok


def closePort():
    """Закрытие COM-порта"""
    global ser
    try:
        ser.close()
    except:
        pass


def writePort(msg):
    """Запись информации в COM-порт

    :param msg: байтовая строка
    """
    global ser
    global lastErrorPort
    try:
        ser.write(msg)
    except:
        if not lastErrorPort:
            print("ОШИБКА: Не удалось отправить информацию в COM-порт.")
            lastErrorPort = True
        closePort()
        openPort()


class SoundThread(Thread):
    """ Класс захвата аудиопотока. Выполняется в отдельном потоке.
    Здесь же происходит быстрое преобразование Фурье.
    """
    def __init__(self):
        Thread.__init__(self)
    
    def run(self):
        print("Start Sound capture thread")
        global gain
        try:
            samplerate = sounddev.query_devices(soundDevice, 'input')['default_samplerate']

            delta_f = (high - low) / (60 - 1)
            fftsize = math.ceil(samplerate / delta_f)
            low_bin = math.floor(low / delta_f)

            # callback-функция, которая вызывается при получении звукового сэмпла
            def callback(indata, frames, time, status):
                global spectrum
                global leftLevel
                global rightLevel

                if status:
                    text = '************************ ' + str(status) + ' ************************'
                    print(text)
                
                # Быстрые преобразования Фурье
                # Левый канал
                magnitude = np.abs(np.fft.rfft(indata[:, 0], n=fftsize))
                magnitude *= gain / fftsize
                leftspectrum = []
                for x in magnitude[low_bin:low_bin + 60]:
                    leftspectrum.append(x * 100000)
                # Правый канал
                magnitude = np.abs(np.fft.rfft(indata[:, 1], n=fftsize))
                magnitude *= gain / fftsize
                rightspectrum = []
                for x in magnitude[low_bin:low_bin + 60]:
                    rightspectrum.append(x * 100000)

                lock_spectrum.acquire()
                leftLevel = max(leftspectrum)
                rightLevel = max(rightspectrum)
                spectrum = []
                for i in range(0, len(leftspectrum)):
                    spectrum.append(max(leftspectrum[i], rightspectrum[i]))
                lock_spectrum.release()

            # Захват звука с аудиоустройства
            with sounddev.InputStream(device=soundDevice, channels=2, callback=callback,
                                blocksize=int(samplerate * block_duration / 1000),
                                samplerate=samplerate):
                while True:
                    time.sleep(1)
                    lock_stop_thread.acquire()
                    if stop_thread:
                        lock_stop_thread.release()
                        break
                    lock_stop_thread.release()
            print("Stop Sound capture thread")

        except KeyboardInterrupt:
            print('Interrupted by user')
        except Exception as e:
            print(type(e).__name__ + ': ' + str(e))


class SystemTrayIcon(QSystemTrayIcon):
    """ Класс значка в системном трее """
    def __init__(self, icon, parent=None):
        QSystemTrayIcon.__init__(self, icon, parent)
        menu = QMenu(parent)
        exitAction = menu.addAction("Exit")
        self.setContextMenu(menu)


class ColormusicApp(QtWidgets.QMainWindow, mainform.Ui_MainWindow):
    """ Класс главного окна приложения """
    def __init__(self):
        super().__init__()
        self.setupUi(self)  # Это нужно для инициализации нашего дизайна

        # Кнопки без фиксации
        self.buttPress = {
            "agbvPlus": [150, 35, 20, 20, "+", False, None],
            "agbvMinus": [80, 35, 20, 20, "-", False, None]
        }

        # Кнопки с фиксацией
        #   X, Y, W, H, текст кнопки, состояние, callback
        self.butt = {
            "OnOff": [10, 10, 50, 20, "ON", False, None],
            "AutoGain": [80, 10, 90, 20, "Auto gain", False, None],
            "LogComp": [180, 10, 90, 20, "Comp", False, None],
            "Strob1": [280, 10, 45, 45, "Strob", False, self.eventStrobButton],
            "Strob2": [330, 10, 45, 45, "Strob", False, self.eventStrobButton],
            "Strob3": [380, 10, 45, 45, "Strob", False, self.eventStrobButton],
            "Strob4": [430, 10, 45, 45, "Strob", False, self.eventStrobButton],
            "Strob5": [480, 10, 45, 45, "Strob", False, self.eventStrobButton]
        }

        # Установка значений элементам управления из текущих настроек
        self.sensR.setValue(settings["sensitivityRYG"][0])
        self.sensY.setValue(settings["sensitivityRYG"][1])
        self.sensG.setValue(settings["sensitivityRYG"][2])

        # Активность стробоскопов
        self.StroboActive = False
        self.StroboTimer = QtCore.QTimer()
        self.StroboTimer.timeout.connect(self.strob)

        # Список для хранения частотного спектра сигнала
        self.spectrum = [0] * 60

        # Для автоматического уровня сигнала
        self.maxvalue = 1
        self.lastMaxPeakTime = time.time()
        self.agBurstValue = 0

        self.sensR.valueChanged.connect(lambda: self.sensitivityChange(0))
        self.sensY.valueChanged.connect(lambda: self.sensitivityChange(1))
        self.sensG.valueChanged.connect(lambda: self.sensitivityChange(2))

        # Данные для 4-х канальной цветомузыки
        # Red, yellow, green, blue
        self.chanRYGB = [False, False, False, False]
        # Таймеры выключения ламп RGBY (R,Y,G)
        self.lamptimer = [QtCore.QTimer(), QtCore.QTimer(), QtCore.QTimer()]
        self.lamptimer[0].timeout.connect(lambda: self.stopLamp(0))
        self.lamptimer[1].timeout.connect(lambda: self.stopLamp(1))
        self.lamptimer[2].timeout.connect(lambda: self.stopLamp(2))

        # Главный таймер
        self.timer = QtCore.QTimer()
        self.timer.timeout.connect(self.on_timer)
        self.timer.start(20)

        openPort()


    def sensitivityChange(self, id):
        """ Event на изменение положения ручек регулировки чувствительности """
        global settings
        if id == 0:
            value = self.sensR.value()
        elif id == 1:
            value = self.sensY.value()
        else:
            value = self.sensG.value()
        # value = (100 - value) * 10
        settings["sensitivityRYG"][id] = value


    def stopLamp(self, index):
        """ Выключение лампы с индексом index """
        self.chanRYGB[index] = False
        self.lamptimer[index].stop()


    def strob(self):
        """ Генерация строба на цветомузыке """
        buf = [0x00]
        for i in range(0, 30):
            buf.append(0xFF)
        try:
            pass
        except AttributeError:
            return


    def eventStrobButton(self, name, state):
        """ Событие нажатия на кнопку стробоскопа """
        for i in range(1, 6):
            s = "Strob" + str(i)
            self.butt[s][5] = False
        if state:
            self.butt[name][5] = True

        if state:
            if name == "Strob1":
                bpm = 60
            elif name == "Strob2":
                bpm = 120
            elif name == "Strob3":
                bpm = 300
            elif name == "Strob4":
                bpm = 600
            elif name == "Strob5":
                bpm = 1200
            else:
                bpm = 60

            period = round(60000 / bpm)
            self.StroboActive = True
            self.StroboTimer.start(period)
        else:
            self.StroboTimer.stop()


    def sendSerial(self):
        """ Отправка данных на устройство """
        c = []
        # Дежурный канал
        if (self.chanRYGB[0] == False) and (self.chanRYGB[1] == False) and (self.chanRYGB[2] == False):
            self.chanRYGB[3] = True
        else:
            self.chanRYGB[3] = False

        value = 0

        for i in range(0, 4):
            if self.chanRYGB[i]:
                value = value | (1 << i)
        
        writePort(bytes([value]))

        

    
    def mousePressEvent(self, QMouseEvent):
        """ Событие нажатия кнопки мыши """
        xx, yy = QMouseEvent.x(), QMouseEvent.y()
        for item in self.butt:
            x, y = self.butt[item][0], self.butt[item][1]
            w, h = self.butt[item][2], self.butt[item][3]
            if (xx >= x) and (xx < x + w) and (yy >= y) and (yy < y + h):
                self.butt[item][5] = not self.butt[item][5]
                proc = self.butt[item][6]
                if proc:
                    proc(item, self.butt[item][5])

        for item in self.buttPress:
            x, y = self.buttPress[item][0], self.buttPress[item][1]
            w, h = self.buttPress[item][2], self.buttPress[item][3]
            if (xx >= x) and (xx < x + w) and (yy >= y) and (yy < y + h):
                self.buttPress[item][5] = True
                if item == "agbvPlus":
                    self.agBurstValue += 5
                    if self.agBurstValue > 70:
                        self.agBurstValue = 70
                elif item == "agbvMinus":
                    self.agBurstValue -= 5
                    if self.agBurstValue < 0:
                        self.agBurstValue = 0

    
    def mouseReleaseEvent(self, QMouseEvent):
        """ Событие отпускания кнопки мыши """
        xx, yy = QMouseEvent.x(), QMouseEvent.y()
        for item in self.buttPress:
            if self.buttPress[item][5]:
                self.buttPress[item][5] = False


    def on_timer(self):
        """ Обработчик события главного таймера.
        Таймер вызывается каждые 20 мс. Здесь происходит обработка данных спектра,
        выполнение алгоритма переключения светодиодов и отрисовка GUI.
        """
        global spectrum

        lock_spectrum.acquire()
        self.spectrum = []
        for x in spectrum:
            self.spectrum.append(x)
        lock_spectrum.release()

        if len(self.spectrum) != 60:
            self.spectrum = [0] * 60

        self.update()

        # Auto gain
        if self.butt["AutoGain"][5]:
            # Если максимальный уровень сигнала не превышает 20, то считаем, что тишина
            #print(max(self.spectrum[0:60]))
            if max(self.spectrum[0:60]) > 50:
                if time.time() - self.lastMaxPeakTime > 15:
                    self.maxvalue = 1

                maxs = max(self.spectrum)
                if maxs > self.maxvalue:
                    self.maxvalue = maxs
                    self.lastMaxPeakTime = time.time()
                gainCorrection = ((self.agBurstValue / 100) * 1000 + 1000) / self.maxvalue
                for i in range(0, 60):
                    self.spectrum[i] = self.spectrum[i] * gainCorrection
            else:
                self.maxvalue = 1
        # ==========================


        # Логарифмический компрессор
        if self.butt["LogComp"][5]:
            for i in range(0, 60):
                try:
                    # Немного математической магии :)
                    self.spectrum[i] = ((self.agBurstValue / 100) * 1000 + 1000) * (1 / math.log10(1000 / 50)) * math.log10(self.spectrum[i] / 50)
                    if self.spectrum[i] < 0:
                        self.spectrum[i] = 0
                except ValueError:
                    self.spectrum[i] = 0
        # ==========================

        #print(self.spectrum)
        #peaks2, _ = find_peaks(self.spectrum, prominence=1)
        #print(peaks2)
        #b, a = scipy.signal.butter(3, 0.1)
        #self.spectrum = scipy.signal.filtfilt(b, a, self.spectrum)
        #print(self.spectrum)

        # Обработка данных для 4 канальной RGBY цветомузыки
        self.processRGBY()
     
        self.sendSerial()


    def paintEvent(self, e):
        """ Обработчик перерисовки формы """
        qp = QPainter()
        qp.begin(self)
        self.drawUI(qp)
        qp.end()


    def drawSimpleRect(self, qp, x1, y1, x2, y2):
        """ Рисует пустой прямоугольник """
        qp.drawLine(x1, y1, x2, y1)
        qp.drawLine(x2, y1, x2, y2)
        qp.drawLine(x2, y2, x1, y2)
        qp.drawLine(x1, y2, x1, y1)


    def drawUI(self, qp):
        """ Рисуем GUI """
        activeColor = QColor(234, 237, 242)
        bgColor = QColor(39, 72, 135)

        # Фон
        qp.setPen(bgColor)
        qp.setBrush(bgColor)
        qp.drawRect(0, 0, 700, 500)

        # Рамки
        qp.setPen(activeColor)
        self.drawSimpleRect(qp, 48, 60, 652, 164)
        self.drawSimpleRect(qp, 11, 60, 35, 164)

        # Спектр сигнала
        qp.setPen(bgColor)
        if len(self.spectrum) != 0:
            maxv = 1000 #max(self.spectrum)

            if maxv < 1000:
                maxv = 1000

            x = 0
            for v in self.spectrum:
                for y in range(0, 10):

                    if v >= (maxv/10)*(y+1):
                        if y >= 8:
                            qp.setBrush(QColor(255, 0, 0))
                        elif y >= 6:
                            qp.setBrush(QColor(255, 255, 0))
                        else:
                            qp.setBrush(QColor(0, 255, 0))
                    else:
                        qp.setBrush(bgColor)
                    qp.drawRect(50 + 10*x, 62 + 10*(9 - y), 9, 9)
                x += 1

            for y in range(0, 10):
                if leftLevel >= (maxv/10)*(y+1):
                    if y >= 8:
                        qp.setBrush(QColor(255, 0, 0))
                    elif y >= 6:
                        qp.setBrush(QColor(255, 255, 0))
                    else:
                        qp.setBrush(QColor(0, 255, 0))
                else:
                    qp.setBrush(bgColor)
                qp.drawRect(13, 62 + 10*(9 - y), 9, 9)

                if rightLevel >= (maxv/10)*(y+1):
                    if y >= 8:
                        qp.setBrush(QColor(255, 0, 0))
                    elif y >= 6:
                        qp.setBrush(QColor(255, 255, 0))
                    else:
                        qp.setBrush(QColor(0, 255, 0))
                else:
                    qp.setBrush(bgColor)
                qp.drawRect(23, 62 + 10*(9 - y), 9, 9)

        # Рисуем кнопки
        for item in self.butt:
            x, y = self.butt[item][0], self.butt[item][1]
            w, h = self.butt[item][2], self.butt[item][3]
            if self.butt[item][5]:
                qp.setBrush(activeColor)
            else:
                qp.setBrush(bgColor)
            qp.setPen(activeColor)
            qp.drawRect(x, y, w, h)

            if self.butt[item][5]:
                qp.setPen(bgColor)
            else:
                qp.setPen(activeColor)
            qp.setFont(QFont('Arial', 10))
            qp.drawText(QRect(x, y, w, h), Qt.AlignCenter, self.butt[item][4])

        for item in self.buttPress:
            x, y = self.buttPress[item][0], self.buttPress[item][1]
            w, h = self.buttPress[item][2], self.buttPress[item][3]
            if self.buttPress[item][5]:
                qp.setBrush(activeColor)
            else:
                qp.setBrush(bgColor)
            qp.setPen(activeColor)
            qp.drawRect(x, y, w, h)

            if self.buttPress[item][5]:
                qp.setPen(bgColor)
            else:
                qp.setPen(activeColor)
            qp.setFont(QFont('Arial', 10))
            qp.drawText(QRect(x, y, w, h), Qt.AlignCenter, self.buttPress[item][4])


        # Текущее состояние светодиодов RGBY
        qp.setPen(activeColor)
        for i in range(0, 4):
            if self.chanRYGB[i]:
                if i == 0:
                    qp.setBrush(QColor(255, 0, 0))
                elif i == 1:
                    qp.setBrush(QColor(255, 255, 0))
                elif i == 2:
                    qp.setBrush(QColor(0, 255, 0))
                else:
                    qp.setBrush(QColor(0, 0, 255))
            else:
                qp.setBrush(QColor(0, 0, 0))
            qp.drawRect(280 + i * 22, 200, 20, 20)

        # Надписи
        qp.setPen(activeColor)
        qp.setFont(QFont('Arial', 10))
        qp.drawText(QRect(100, 35, 50, 20), Qt.AlignCenter, str(self.agBurstValue) + "%")
        qp.drawText(QRect(self.sensR.x(), self.sensR.y() - 10, self.sensR.width(), 10), Qt.AlignCenter, str(settings["sensitivityRYG"][0]))
        qp.drawText(QRect(self.sensY.x(), self.sensY.y() - 10, self.sensY.width(), 10), Qt.AlignCenter, str(settings["sensitivityRYG"][1]))
        qp.drawText(QRect(self.sensG.x(), self.sensG.y() - 10, self.sensG.width(), 10), Qt.AlignCenter, str(settings["sensitivityRYG"][2]))


    def processRGBY(self):
        """ Обработка спектра для вывода на цветомузыку. """
        if len(self.spectrum) == 0:
            return
        ch = [0, 0, 0]
        ch[0] = max(self.spectrum[0:4])
        ch[1] = max(self.spectrum[4:8])
        ch[2] = max(self.spectrum[8:13])

        #ch[0] = max(self.spectrum[0:2])
        #ch[1] = max(self.spectrum[2:4])
        #ch[2] = max(self.spectrum[4:8])
        #ch[3] = max(self.spectrum[8:14])
        #ch[4] = max(self.spectrum[14:30])

        # Если превышен порог, то выставляем флаг включения лампы и запускаем таймер, который её затем выключит
        for i in range(0, 3):
            value = (128 - settings["sensitivityRYG"][i]) * 7.8125
            if ch[i] > value:
                self.chanRYGB[i] = True
                self.lamptimer[i].start(100)
            else:
                pass
                #self.chanRYGB[i] = False



def main():
    global stop_thread
    global datapath

    if isWindows():
        datapath = os.getenv('APPDATA') + "\\ColormusicCC\\"
        if not os.path.exists(datapath):
            os.mkdir(datapath)

    loadSettings()

    app = QtWidgets.QApplication(sys.argv)
    window = ColormusicApp()
    window.show()

    #print(str(sounddev.query_devices()).split('\n'))

    sound_thread = SoundThread()
    sound_thread.start()

    w = QWidget()
    trayIcon = SystemTrayIcon(QtGui.QIcon("images\\tray.png"), w)
    trayIcon.show()

    app.exec_()  # и запускаем приложение
    
    # Отправляем потокам сообщение о необходимости остановки
    lock_stop_thread.acquire()
    stop_thread = True
    lock_stop_thread.release()
    # Ожидание завершения потока
    sound_thread.join()
    
    saveSettings()

if __name__ == '__main__':
    main()